package com.lcz.init;

import cn.hutool.crypto.digest.MD5;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.lcz.common.MediaConstant;
import com.lcz.dto.Camera;
import com.lcz.server.FlvHandler;
import com.lcz.service.CameraRepository;
import com.lcz.service.MediaService;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.unix.PreferredDirectByteBufAllocator;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.cors.CorsConfig;
import io.netty.handler.codec.http.cors.CorsConfigBuilder;
import io.netty.handler.codec.http.cors.CorsHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.File;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.List;

/**
 * @author lcz
 * @desc 启动流媒体
 * @mail lcz_0130@163.com
 * @date 2024/10/16
 */
@Slf4j
@Component
public class InitServer implements CommandLineRunner {

    @Value("${mediaserver.port}")
    private int port;

    @Autowired
    private FlvHandler flvHandler;
    @Autowired
    private MediaService mediaService;

    @Autowired
    private CameraRepository cameraRepository;

    @Override
    public void run(String... args) throws Exception {
        initJsonCamera();
        new Thread(() -> start(new InetSocketAddress("0.0.0.0", port))).start();

    }

    /**
     * flv流媒体服务
     */
    public void start(InetSocketAddress socketAddress) {
        //new 一个主线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //new 一个工作线程组
        EventLoopGroup workGroup = new NioEventLoopGroup(200);
        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin().allowNullOrigin().allowCredentials().build();

                        socketChannel.pipeline()
                                .addLast(new HttpResponseEncoder())
                                .addLast(new HttpRequestDecoder())
                                .addLast(new ChunkedWriteHandler())
                                .addLast(new HttpObjectAggregator(64 * 1024))
                                .addLast(new CorsHandler(corsConfig))
                                .addLast(flvHandler);
                    }
                })
                .localAddress(socketAddress)
                .option(ChannelOption.SO_BACKLOG, 128)
                //首选直接内存
                .option(ChannelOption.ALLOCATOR, PreferredDirectByteBufAllocator.DEFAULT)
                //设置队列大小
//                .option(ChannelOption.SO_BACKLOG, 1024)
                // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.SO_RCVBUF, 128 * 1024)
                .childOption(ChannelOption.SO_SNDBUF, 1024 * 1024)
                .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(1024 * 1024 / 2, 1024 * 1024));
        //绑定端口,开始接收进来的连接
        try {
            ChannelFuture future = bootstrap.bind(socketAddress).sync();
            log.info("流媒体服务启动开始监听端口: {}", socketAddress.getPort());
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //关闭主线程组
            bossGroup.shutdownGracefully();
            //关闭工作线程组
            workGroup.shutdownGracefully();
        }
    }

    /**
     * 初始化本地json
     */
    public void initJsonCamera() {
        ApplicationHome applicationHome = new ApplicationHome();
        File file = new File(applicationHome.getDir() + File.separator + "camera.json");
        if (file.exists()) {
            log.info("发现camera.json，已启动自动拉流！");
            JSONArray readJSONArray = JSONUtil.readJSONArray(file, Charset.forName("utf-8"));
            List<Camera> list = JSONUtil.toList(readJSONArray, Camera.class);
            cameraRepository.readDataToMap(list);

            for (Camera camera : CameraRepository.cameraMap.values()) {
                // 区分不同媒体
                String mediaKey = MD5.create().digestHex(camera.getUrl());
                camera.setMediaKey(mediaKey);
                //已启用的自动拉流，不启用的不自动拉
                if (camera.isEnabledFlv()) {
                    mediaService.playForApi(camera);
                }
            }
        } else {
            log.info("未发现camera.json，您可以通过restful api添加或删除流！");
        }
    }

    /**
     * 提前初始化，可避免推拉流启动耗时太久
     */
    @PostConstruct
    public void loadFFmpeg() {
        try {
            log.info("正在初始化资源，请稍等...");
            FFmpegFrameGrabber.tryLoad();
            FFmpegFrameRecorder.tryLoad();
        } catch (org.bytedeco.javacv.FrameGrabber.Exception e) {
            e.printStackTrace();
        } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
            e.printStackTrace();
        }

        /**
         * 初始化ffmpeg路径
         */
        String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
        System.setProperty(MediaConstant.ffmpegPathKey, ffmpeg);
        log.info("初始化成功");
    }
}
